Sending and Landing Transactions
- Rust & Typescript Kit
- Typescript Legacy
In this guide, we'll explore how to send transactions to the Solana blockchain for any Solana project. We'll cover two approaches:
- Using the simplified
tx-sender
library - a lightweight solution that works with any Solana project - The manual approach using Solana's native SDKs directly
The Easy Way: Using tx-sender
The tx-sender
library is a lightweight package designed to simplify transaction building and sending in Solana. It handles all the complex aspects like priority fees, Jito tips, compute unit estimation, and retry logic automatically.
Installation
- Rust
- Typescript Kit
[dependencies]
orca_tx_sender = { version = "^0.1.0" }
"dependencies": {
"@orca-so/tx-sender": "^1.0.0"
},
Usage
- Rust
- Typescript Kit
use orca_tx_sender::{
build_and_send_transaction,
set_rpc, get_rpc_client
};
use solana_sdk::signature::Signer;
use solana_sdk::commitment_config::CommitmentLevel;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize RPC configuration (required!)
set_rpc("https://api.mainnet-beta.solana.com").await?;
// Get instructions from Whirlpools SDK
let instructions_result = // your whirlpool instructions here
// Build and send transaction
let signature = build_and_send_transaction(
instructions_result.instructions,
&[&wallet], // signers array including your wallet and any additional signers
Some(CommitmentLevel::Confirmed),
None, // No address lookup tables
).await?;
println!("Transaction sent: {}", signature);
Ok(())
}
import {
setRpc,
setPriorityFeeSetting,
setJitoTipSetting,
buildAndSendTransaction
} from "@orca-so/tx-sender";
import { createSignerFromKeypair } from "@solana/kit";
// Initialize RPC connection (required)
await setRpc("https://api.mainnet-beta.solana.com");
// Get instructions from Whirlpools SDK
const { instructions } = // your whirlpool instructions here
// Build and send transaction with default fee settings
const signature = await buildAndSendTransaction(
instructions,
wallet // your wallet signer
);
console.log(`Transaction confirmed: ${signature}`);
Configuration Options
The tx-sender library provides flexible configuration options to optimize your transaction sending strategy. Let's break these down in detail:
Default Settings
By default, tx-sender uses the following configuration:
- Priority Fees: Dynamic pricing with a max cap of 0.004 SOL (4,000,000 lamports)
- Jito Tips: Dynamic pricing with a max cap of 0.004 SOL (4,000,000 lamports)
- Compute Unit Margin: 1.1x multiplier for compute unit calculation (10% margin)
- Jito Block Engine URL:
https://bundles.jito.wtf
Priority Fee Configuration
Priority fees incentivize validators to include your transaction in blocks. The tx-sender library supports three priority fee strategies:
Understanding Dynamic Fees and Percentiles
When using the "dynamic" fee strategy, tx-sender automatically analyzes recent network conditions to determine an appropriate fee. The system works by:
- Calling the
getRecentPrioritizationFees
RPC method, which returns data about fees from the last 150 blocks - Sorting these fees from lowest to highest
- Selecting a specific percentile from this data
The tx-sender library allows capping these dynamic fees at a maximum amount to prevent excessive spending during extreme network conditions.
Note: The tx-sender library automatically filters out zero-fee transactions before calculating percentiles. This ensures that during periods of low network activity when many blocks have zero fees, your transaction still has an appropriate non-zero fee to improve landing probability.
- Rust
- Typescript Kit
// 1. Dynamic fees - automatically adjusts based on network conditions
set_priority_fee_strategy(PriorityFeeStrategy::Dynamic {
percentile: Percentile::P75, // Options: P25, P50, P75, P95, P99
max_lamports: 5_000_000, // Optional: Cap at 0.005 SOL (default: 4,000,000)
})?;
// 2. Exact fees - specify an exact amount
set_priority_fee_strategy(PriorityFeeStrategy::Exact(10_000))?; // Exactly 0.00001 SOL
// 3. No priority fees
set_priority_fee_strategy(PriorityFeeStrategy::Disabled)?;
// 1. Dynamic fees - automatically adjusts based on network conditions
setPriorityFeeSetting({
type: "dynamic",
maxCapLamports: BigInt(5_000_000), // Optional: Cap at 0.005 SOL (default: 4,000,000)
});
// 2. Exact fees - specify an exact amount
setPriorityFeeSetting({
type: "exact",
amountLamports: BigInt(10_000), // Exactly 0.00001 SOL
});
// 3. No priority fees
setPriorityFeeSetting({
type: "none",
});
// Set a specific priority fee percentile (applicable for dynamic fees)
// Available values: "25", "50", "75", "95", "99"
setPriorityFeePercentile("75"); // Use 75th percentile (default: "50")
Jito Tip Configuration
Jito tips are additional fees that go to Jito MEV (Maximal Extractable Value) validators, who represent approximately 85% of Solana's validator stake. These tips can improve transaction landing probability even further than regular priority fees.
Understanding Jito Tips and Dynamic Pricing
Jito tips work similarly to priority fees but are specifically for Jito validators. When using dynamic Jito tips:
- The percentile system works the same way as with priority fees - selecting from recent fee data
- Jito offers an additional option: "50ema" (Exponential Moving Average), which smooths out fee spikes by using a weighted average
- Jito tips are sent directly to the Jito block engine rather than through the regular fee mechanism
Using Jito tips is particularly effective because:
- Jito validators account for about 85% of Solana's stake weight
- They use a specialized searching algorithm to look for higher-tipped transactions
- During congestion, Jito validators can help your transaction land faster
Like priority fees, Jito tips can be capped to prevent excessive spending. The default cap is 4,000,000 lamports (0.004 SOL).
- Rust
- Typescript Kit
// 1. Dynamic Jito tips
set_jito_fee_strategy(JitoFeeStrategy::Dynamic {
percentile: JitoPercentile::P50Ema, // P25, P50, P75, P95, P99, P50Ema
max_lamports: 3_000_000, // Optional: Cap at 0.003 SOL
})?;
// 2. Exact Jito tip
set_jito_fee_strategy(JitoFeeStrategy::Exact(20_000))?; // Exactly 0.00002 SOL
// 3. No Jito tips
set_jito_fee_strategy(JitoFeeStrategy::Disabled)?;
// Set custom Jito block engine URL (defaults to "https://bundles.jito.wtf")
set_jito_block_engine_url("https://your-jito-service.com")?;
// 1. Dynamic Jito tips
setJitoTipSetting({
type: "dynamic",
maxCapLamports: BigInt(3_000_000), // Optional: Cap at 0.003 SOL (default: 4,000,000)
});
// 2. Exact Jito tip
setJitoTipSetting({
type: "exact",
amountLamports: BigInt(20_000), // Exactly 0.00002 SOL
});
// 3. No Jito tips
setJitoTipSetting({
type: "none",
});
// Set a specific Jito fee percentile or use EMA
// Available values: "25", "50", "75", "95", "99", "50ema"
setJitoFeePercentile("50ema"); // Use 50th percentile exponential moving average
// Set custom Jito block engine URL (defaults to "https://bundles.jito.wtf")
setJitoBlockEngineUrl("https://your-jito-service.com");
Compute Unit Configuration
The compute units represent the computational resources your transaction requires. The margin multiplier adds extra units as a safety buffer to prevent transaction failures.
How Compute Unit Estimation Works
When sending a transaction, tx-sender performs these steps to optimize compute unit usage:
- First, it simulates your transaction on the RPC to estimate the required compute units
- Then, it applies the margin multiplier to add a safety buffer (default is 1.1, or 10% extra)
- Finally, it sets a compute unit limit instruction at the beginning of your transaction
This process ensures that your transaction:
- Has enough compute units to complete execution
- Doesn't allocate unnecessarily high compute units (which would cost more in fees)
- Has a safety margin to account for differences between simulation and actual execution
Setting an appropriate margin is important because:
- Too low (close to 1.0): Transaction might fail with "out of compute units" error if network conditions change
- Too high (over 1.5): Transactions that request higher compute units get lower priority for the same prioritization fee. Higher compute units signal to validators that your transaction will use more resources.
For most transactions, a value between 1.1 and 1.2 (10-20% margin) is appropriate. For complex or unpredictable transactions, you might want to use a higher value like 1.3 or 1.4.
- Rust
- Typescript Kit
margin) // Default is 1.1 (10% margin)
set_compute_unit_margin_multiplier(1.2)?; // 20% margin ```
</TabItem>
<TabItem value="ts-kit" label="Typescript Kit" default>
```tsx // Values typically range from 1.0 (no margin) to 2.0 (100% extra
margin) // Default is 1.1 (10% margin) setComputeUnitMarginMultiplier(1.2);
// 20% margin for compute units ```
</TabItem>
</Tabs>
#### RPC Configuration
<Tabs groupId="programming-languages">
<TabItem value="rust" label="Rust">
```rust // Basic RPC configuration
set_rpc("https://api.mainnet-beta.solana.com").await?; // Get the configured
RPC client for other operations let client = get_rpc_client()?; ```
</TabItem>
<TabItem value="ts-kit" label="Typescript Kit" default>
```tsx // Basic RPC configuration await
setRpc("https://api.mainnet-beta.solana.com"); // For RPC providers that
support percentile-based priority fees (e.g., Triton) await
setRpc("https://triton.rpcpool.com/some_endpoint", true); ```
</TabItem>
</Tabs>
#### Example: Comprehensive Configuration
Here's an example of a complete configuration setup:
<Tabs groupId="programming-languages">
<TabItem value="rust" label="Rust">
```rust
use orca_tx_sender::{
build_and_send_transaction,
PriorityFeeStrategy, JitoFeeStrategy,
Percentile, JitoPercentile,
set_priority_fee_strategy, set_jito_fee_strategy,
set_compute_unit_margin_multiplier, set_jito_block_engine_url,
set_rpc, get_rpc_client
};
use solana_sdk::commitment_config::CommitmentLevel;
// 1. Set up RPC connection
set_rpc("https://api.mainnet-beta.solana.com").await?;
// 2. Configure priority fees
set_priority_fee_strategy(PriorityFeeStrategy::Dynamic {
percentile: Percentile::P75,
max_lamports: 5_000_000, // 0.005 SOL
})?;
// 3. Configure Jito tips
set_jito_fee_strategy(JitoFeeStrategy::Dynamic {
percentile: JitoPercentile::P50Ema,
max_lamports: 3_000_000, // 0.003 SOL
})?;
// 4. Set compute unit margin
set_compute_unit_margin_multiplier(1.2)?;
// 5. Optional: Custom Jito endpoint
set_jito_block_engine_url("https://bundles.jito.wtf")?;
// 6. Send transaction with configured settings
let signature = build_and_send_transaction(
instructions,
&[&wallet],
Some(CommitmentLevel::Confirmed),
None
).await?;
import {
setRpc,
setPriorityFeeSetting,
setPriorityFeePercentile,
setJitoTipSetting,
setJitoFeePercentile,
setComputeUnitMarginMultiplier,
setJitoBlockEngineUrl,
buildAndSendTransaction
} from "@orca-so/tx-sender";
// 1. Set up RPC connection
await setRpc("https://api.mainnet-beta.solana.com");
// 2. Configure priority fees
setPriorityFeeSetting({
type: "dynamic",
maxCapLamports: BigInt(5_000_000), // 0.005 SOL
});
setPriorityFeePercentile("75");
// 3. Configure Jito tips
setJitoTipSetting({
type: "dynamic",
maxCapLamports: BigInt(3_000_000), // 0.003 SOL
});
setJitoFeePercentile("50ema");
// 4. Set compute unit margin
setComputeUnitMarginMultiplier(1.2);
// 5. Optional: Custom Jito endpoint
setJitoBlockEngineUrl("https://bundles.jito.wtf");
// 6. Send transaction with configured settings
const signature = await buildAndSendTransaction(
instructions,
wallet
);
The Manual Way: Using Solana SDKs Directly
In this section, we'll explore how to send the instructions using the Solana SDK directly - both in Typescript and Rust. We'll cover the following key topics:
- Client-side retry
- Prioritization fees
- Compute budget estimation
We also cover key considerations for sending transactions in web applications with wallet extensions, along with additional steps to improve transaction landing.
Code Overview
1. Dependencies
Let's start by importing the necessary dependencies from Solana's SDKs.
- Rust
- Typescript Kit
serde_json = { version = "^1.0" }
solana-client = { version = "^1.18" }
solana-sdk = { version = "^1.18" }
tokio = { version = "^1.41.1" }
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_client::rpc_config::RpcSendTransactionConfig;
use solana_sdk::commitment_config::CommitmentLevel;
use solana_sdk::compute_budget::ComputeBudgetInstruction;
use solana_sdk::message::Message;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::Signature;
use solana_sdk::transaction::Transaction;
use solana_sdk::{signature::Keypair, signer::Signer};
use std::fs;
use std::str::FromStr;
use tokio::time::{sleep, Duration, Instant};
"dependencies": {
"@solana-program/compute-budget": "^0.6.1",
"@solana/kit": "^2.1.0",
},
import {
createSolanaRpc,
address,
pipe,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
prependTransactionMessageInstructions,
signTransactionMessageWithSigners,
getComputeUnitEstimateForTransactionMessageFactory,
getBase64EncodedWireTransaction,
setTransactionMessageFeePayerSigner
} from '@solana/kit';
import {
getSetComputeUnitLimitInstruction,
getSetComputeUnitPriceInstruction
} from '@solana-program/compute-budget';
2. Create Transaction Message From Instructions
To send a transaction on Solana, you need to include a blockhash to the transaction. A blockhash acts as a timestamp and ensures the transaction has a limited lifetime. Validators use the blockhash to verify the recency of a transaction before including it in a block. A transaction referencing a blockhash is only valid for 150 blocks (~1-2 minutes, depending on slot time). After that, the blockhash expires, and the transaction will be rejected.
Durable Nonces: In some cases, you might need a transaction to remain valid for longer than the typical blockhash lifespan, such as when scheduling future payments or collecting multi-signature approvals over time. In that case, you can use durable nonces to sign the transaction, which includes a nonce in place of a recent blockhash.
You also need to add the signers to the transactions. With Solana Kit, you can create instructions and add additional signers as TransactionSigner
to the instructions. The Typescript Whirlpools SDK leverages this functioanlity and appends all additional signers to the instructions for you. In Rust, this feautures is not available. Therefore, the Rust Whirlpools SDK may return instruction_result.additional_signers
if there are any, and you need to manually append them to the transaction.
Here's how the transaction message is created:
- Rust
- Typescript Kit
#[tokio::main]
async fn main() {
// ...
let instructions_result = // get instructions from Whirlpools SDK
let message = Message::new(
&instructions_result.instructions,
Some(&wallet.pubkey()),
);
let mut signers: Vec<&dyn Signer> = vec![&wallet];
signers.extend(
instructions_result
.additional_signers
.iter()
.map(|kp| kp as &dyn Signer),
);
let recent_blockhash = rpc.get_latest_blockhash().await.unwrap();
let transaction = Transaction::new(&signers, message, recent_blockhash);
// ...
}
const { instructions } = // get instructions from Whirlpools SDK
const latestBlockHash = await rpc.getLatestBlockhash().send();
const transactionMessage = await pipe(
createTransactionMessage({ version: 0}),
tx => setTransactionMessageFeePayer(wallet.address, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockHash.value, tx),
tx => appendTransactionMessageInstructions(instructions, tx)
)
3. Estimating Compute Unit Limit and Prioritization Fee
Before sending a transaction, it's important to set a compute unit limit and an appropriate prioritization fee.
Transactions that request fewer compute units get high priority for the same amount of prioritization fee (which is defined per compute unit). Setting the compute units too low will result in a failed transaction.
You can get an estimate of the compute units by simulating the transaction on the RPC. To avoid transaction failures caused by underestimating this limit, you can add an additional 100,000 compute units, but you can adjust this based on your own tests.
The prioritization fee per compute unit also incentivizes validators to prioritize your transaction, especially during times of network congestion. You can call the getRecentPrioritizationFees
RPC method to retrieve an array of 150 values, where each value represents the lowest priority fee paid for transactions that landed in each of the past 150 blocks. In this example, we sort that list and select the 50th percentile, but you can adjust this if needed. The prioritization fee is provided in micro-lamports per compute unit. The total priority fee in lamports you will pay is calculated as .
- Rust
- Typescript Kit
#[tokio::main]
async fn main() {
// ...
let simulated_transaction = rpc.simulate_transaction(&transaction).await.unwrap();
let mut all_instructions = vec![];
if let Some(units_consumed) = simulated_transaction.value.units_consumed {
let units_consumed_safe = units_consumed as u32 + 100_000;
let compute_limit_instruction =
ComputeBudgetInstruction::set_compute_unit_limit(units_consumed_safe);
all_instructions.push(compute_limit_instruction);
let prioritization_fees = rpc
.get_recent_prioritization_fees(&[whirlpool_address])
.await
.unwrap();
let mut prioritization_fees_array: Vec<u64> = prioritization_fees
.iter()
.map(|fee| fee.prioritization_fee)
.collect();
prioritization_fees_array.sort_unstable();
let prioritization_fee = prioritization_fees_array
.get(prioritization_fees_array.len() / 2)
.cloned();
if let Some(prioritization_fee) = prioritization_fee {
let priority_fee_instruction =
ComputeBudgetInstruction::set_compute_unit_price(prioritization_fee);
all_instructions.push(priority_fee_instruction);
}
}
// ...
}
const getComputeUnitEstimateForTransactionMessage =
getComputeUnitEstimateForTransactionMessageFactory({
rpc
});
const computeUnitEstimate = await getComputeUnitEstimateForTransactionMessage(transactionMessage) + 100_000;
const medianPrioritizationFee = await rpc.getRecentPrioritizationFees()
.send()
.then(fees =>
fees
.map(fee => Number(fee.prioritizationFee))
.sort((a, b) => a - b)
[Math.floor(fees.length / 2)]
);
const transactionMessageWithComputeUnitInstructions = await prependTransactionMessageInstructions([
getSetComputeUnitLimitInstruction({ units: computeUnitEstimate }),
getSetComputeUnitPriceInstruction({ microLamports: medianPrioritizationFee })
], transactionMessage);
4. Sign and Submit Transaction
Finally, the transaction needs to be signed, encoded, and submitted to the network. A client-side time-base retry mechanism ensures that the transaction is repeatedly sent until it is confirmed or the time runs out. We use a time-based loop, because we know that the lifetime of a transaction is 150 blocks, which on average takes about 79-80 seconds. The signing of the transactions is an idempotent operation and produces a transaction hash, which acts as the transaction ID. Since transactions can be added only once to the block chain, we can keep sending the transaction during the lifetime of the trnsaction.
You're probably wondering why we don't just use the widely used sendAndConfirm
method. This is because the retry mechanism of the sendAndConfirm
method is executed on the RPC. By default, RPC nodes will try to forward (rebroadcast) transactions to leaders every two seconds until either the transaction is finalized, or the transaction's blockhash expires. If the outstanding rebroadcast queue size is greater than 10,000 transaction, newly submitted transactions are dropped. This means that at times of congestion, your transaction might not even arrive at the RPC in the first place. Moreover, the confirmTransaction
RPC method that sendAndConfirm
calls is deprecated.
- Rust
- Typescript Kit
#[tokio::main]
async fn main() {
// ...
all_instructions.extend(open_position_instructions.instructions);
let message = Message::new(&all_instructions, Some(&wallet.pubkey()));
let transaction = Transaction::new(&signers ,message , recent_blockhash);
let transaction_config = RpcSendTransactionConfig {
skip_preflight: true,
preflight_commitment: Some(CommitmentLevel::Confirmed),
max_retries: Some(0),
..Default::default()
};
let start_time = Instant::now();
let timeout = Duration::from_secs(90);
let send_transaction_result = loop {
if start_time.elapsed() >= timeout {
break Err(Box::<dyn std::error::Error>::from("Transaction timed out"));
}
let transaction_start_time = Instant::now();
let signature: Signature = rpc
.send_transaction_with_config(&transaction, transaction_config)
.await
.unwrap();
let statuses = rpc
.get_signature_statuses(&[signature])
.await
.unwrap()
.value;
if let Some(status) = statuses[0].clone() {
break Ok((status, signature));
}
let elapsed_time = transaction_start_time.elapsed();
let remaining_time = Duration::from_millis(1000).saturating_sub(elapsed_time);
if remaining_time > Duration::ZERO {
sleep(remaining_time).await;
}
};
let signature = send_transaction_result.and_then(|(status, signature)| {
if let Some(err) = status.err {
Err(Box::new(err))
} else {
Ok(signature)
}
});
println!("Result: {:?}", signature);
}
const signedTransaction = await signTransactionMessageWithSigners(transactionMessageWithComputeUnitInstructions)
const base64EncodedWireTransaction = getBase64EncodedWireTransaction(signedTransaction);
const timeoutMs = 90000;
const startTime = Date.now();
while (Date.now() - startTime < timeoutMs) {
const transactionStartTime = Date.now();
const signature = await rpc.sendTransaction(base64EncodedWireTransaction, {
maxRetries: 0n,
skipPreflight: true,
encoding: 'base64'
}).send();
const statuses = await rpc.getSignatureStatuses([signature]).send();
if (statuses.value[0]) {
if (!statuses.value[0].err) {
console.log(`Transaction confirmed: ${signature}`);
break;
} else {
console.error(`Transaction failed: ${statuses.value[0].err.toString()}`);
break;
}
}
const elapsedTime = Date.now() - transactionStartTime;
const remainingTime = Math.max(0, 1000 - elapsedTime);
if (remainingTime > 0) {
await new Promise(resolve => setTimeout(resolve, remainingTime));
}
}
Handling transactions with Wallets in web apps.
Creating Noop Signers
When sending transactions from your web application, users need to sign the transaction using their wallet. Since the transaction needs to assembled beforehand, you can create a noopSigner
(no-operation signer) and add it to the instructions. This will act as a placeholder for you instructions, indicating that a given account is a signer and the signature wil be added later. After assembling the transaction you can pass it to the wallet extension. If the user signs, it will return a serialized transaction with the added signature.
Prioritization Fees
Some wallets will calculate and apply priority fees for your transactions, provided:
- The transaction does not already have signatures present.
- The transaction does not have existing compute-budget instructions.
- The transactions will still be less than the maximum transaction size fo 1232 bytes, after applying compute-budget instructions.
Additional Improvements for Landing Transactions
- You could send your transaction to multiple RPC nodes at the same time, all within each iteration of the time-based loop.
- At the time of writing, 85% of Solana validators are Jito validators. Jito validators happily accept an additional tip, in the form a SOL transfer, to prioritize a transaction. A good place to get familiarized with Jito is here: https://www.jito.network/blog/jito-solana-is-now-open-source/
- Solana gives staked validators more reliable performance when sending transactions by routing them through prioritized connections. This mechanism is referred to as stake-weighted Quality of Service (swQoS). Validators can extend this service to RPC nodes, essentially giving staked connections to RPC nodes as if they were validators with that much stake in the network. RPC providers, like Helius and Titan, expose such peered RPC nodes to paid users, allowing users to send transactions to RPC nodes which use the validator's staked connections. From the RPC, the transaction is then sent over the staked connection with a lower likelihood of being delayed or dropped.
Composing your own Transaction
Use the TransactionBuilder class to construct and compose your own Transactions to perform actions on the Whirlpools contract.
Whirlpools Instruction Set
🔗 Whirlpools Instruction Set
https://dev.orca.so/legacy/classes/WhirlpoolIx.html
const txBuilder = new TransactionBuilder(ctx.provider);
txBuilder.addInstruction(WhirlpoolIx.initializeConfigIx(ctx, configParams));
...
txBuilder.addInstruction(otherIx).addSigner(otherSigner);
...
await txBuilder.buildAndExecute();
Use to toTx
function if you only have one transaction to send out
const tx = toTx(ctx, WhirlpoolIx.initializeConfigIx(ctx.program, configParams));
await txBuilder.buildAndExecute();
Add priority fees, compute unit limits, and jito tips
await txBuilder.buildAndExecute({computeUnitBudget: "auto"});